#!/usr/bin/env python3
# SPDX-License-Identifier: MulanPSL-2.0+
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.

"""
Mid-Point Performance Bisect Step Script

This script is called by `git bisect run` for each commit during performance bisect.
It evaluates commit performance against a calculated mid_point threshold.

Logic:
- If commit_sample > mid_point → return GOOD (exit code 0)
- If commit_sample ≤ mid_point → return BAD (exit code 1)

Usage:
    bisect_midpoint_step.py <job_id> <metric> <result_root> <mid_point> <session_id>

Exit codes:
    0 (GOOD): Performance above mid_point
    1 (BAD):  Performance at or below mid_point
    125 (SKIP): Unable to evaluate (missing data, errors)
    -1 (ERROR): Script execution error
"""

import os
import sys
import subprocess
import json
import traceback
from py_bisect import GitBisect
from bisect_log_config import logger, get_logger

# Git bisect exit codes
BISECT_STEP_GOOD = 0
BISECT_STEP_BAD = 1
BISECT_STEP_ERROR = -1
BISECT_STEP_SKIP = 125


class MidPointBisectRun:
    """Handles mid-point based bisect step evaluation"""

    def __init__(self, job_id: str, metric: str, mid_point: float, gitbisect: GitBisect, logger):
        self.job_id = job_id
        self.metric = metric
        self.mid_point = mid_point
        self.gitbisect = gitbisect
        self.logger = logger
        self.commit = None

    def init_bisect_commit(self) -> dict:
        """
        Initialize bisect job content with current commit

        Returns:
            dict: Job configuration with current commit
        """
        self.logger.info(f"Current working directory: {os.getcwd()}")
        job = self.gitbisect.init_job_content(self.job_id)

        # Detect and set task type (critical for error judgment)
        self.gitbisect.is_build_task = self.gitbisect._detect_build_task(job)
        self.logger.info(f"Task type: {'build/compile' if self.gitbisect.is_build_task else 'functional/performance test'}")

        self.logger.debug("Base job initialized", job_id=self.job_id)

        try:
            # Get current commit from git
            work_dir = os.getcwd()
            # Support both --no-checkout (bare repo) and normal checkout modes
            try:
                # Try BISECT_HEAD first (for --no-checkout mode)
                commit = subprocess.check_output(
                    ['git', '-C', work_dir, 'rev-parse', 'BISECT_HEAD'],
                    universal_newlines=True
                ).strip()
            except subprocess.CalledProcessError:
                # Fallback to HEAD (for normal checkout mode)
                commit = subprocess.check_output(
                    ['git', '-C', work_dir, 'rev-parse', 'HEAD'],
                    universal_newlines=True
                ).strip()

            self.commit = commit

            self.logger.info(f"Current bisect commit: {commit}")

            # Update job with current commit
            if job.get('ss'):
                job['ss']['linux']['commit'] = commit
                # Invalidate cached MD5 for ss jobs
                if 'all_params_md5' in job:
                    del job['all_params_md5']
            elif job.get('program', {}).get('makepkg'):
                job['program']['makepkg']['commit'] = commit

                # CRITICAL: Also update pp structure (used for MD5 calculation)
                if 'pp' in job and 'makepkg' in job['pp']:
                    job['pp']['makepkg']['commit'] = commit
                else:
                    self.logger.warning(f"pp.makepkg not found in job, MD5 may be incorrect!")

                # Invalidate cached MD5 so it gets recalculated with new commit
                if 'all_params_md5' in job:
                    del job['all_params_md5']
            else:
                # For performance bisect, create minimal structure
                job['ss'] = {'linux': {'commit': commit}}

            return job

        except subprocess.CalledProcessError as e:
            self.logger.exception("Failed to get current commit", error=str(e))
            job['upstream_commit'] = None
            return job

    def evaluate_commit_performance(self, job: dict) -> None:
        """
        Submit job and evaluate performance against mid_point

        Args:
            job: Job configuration with current commit

        Exits with appropriate bisect code
        """
        try:
            self.logger.info("=== Mid-Point Bisect Step Evaluation ===")
            self.logger.info(f"Job ID: {self.job_id}")
            self.logger.info(f"Metric: {self.metric}")
            self.logger.info(f"Mid-point threshold: {self.mid_point}")

            # Submit job and wait for completion
            status = self.gitbisect.submit_wait_job_status(
                job=job,
                stat=self.metric
            )

            self.logger.info(f"Job evaluation status: {status} for commit {self.commit}")

            # Map status to bisect exit codes
            if status == 'good':
                self.logger.info(f"Performance ABOVE mid_point → GOOD commit: {self.commit}")
                sys.exit(BISECT_STEP_GOOD)
            elif status == 'bad':
                self.logger.info(f"Performance AT OR BELOW mid_point → BAD commit: {self.commit}")
                sys.exit(BISECT_STEP_BAD)
            elif status == 'skip':
                self.logger.warning(f"Unable to evaluate performance → SKIP commit: {self.commit}")
                sys.exit(BISECT_STEP_SKIP)
            else:
                self.logger.error(f"Unknown evaluation status: {status} for commit {self.commit}")
                sys.exit(BISECT_STEP_ERROR)

        except Exception as e:
            self.logger.exception("Performance evaluation failed",
                                error=str(e),
                                job_id=self.job_id,
                                metric=self.metric,
                                mid_point=self.mid_point)
            traceback.print_exc(file=sys.stderr)
            sys.exit(BISECT_STEP_ERROR)


def main():
    """
    Main entry point for mid-point bisect step

    Command line arguments:
        1. job_id: Base job ID for testing
        2. metric: Performance metric name
        3. result_root: Results directory
        4. mid_point: Performance threshold value
        5. direction: Performance direction (1 for higher is better, -1 for lower is better)
        6. session_id: Bisect session ID
    """
    try:
        # Parse command line arguments
        if len(sys.argv) < 7:
            raise IndexError("Insufficient parameters")

        job_id = sys.argv[1]
        metric = sys.argv[2]
        result_root = os.path.abspath(sys.argv[3])
        mid_point = float(sys.argv[4])
        direction = int(sys.argv[5])
        session_id = sys.argv[6] if len(sys.argv) > 6 else None

        # Validate required parameters
        if not job_id or not job_id.strip():
            raise ValueError("job_id cannot be empty")
        if not metric or not metric.strip():
            raise ValueError("metric cannot be empty")
        if mid_point is None:
            raise ValueError("mid_point must be provided")
        if direction not in [1, -1]:
            raise ValueError("direction must be 1 or -1")

        # Set up logging
        logger = get_logger(session_id=session_id, log_dir=result_root)
        actual_session_id = getattr(logger, 'session_id', session_id)
        os.makedirs(result_root, exist_ok=True)

        # Log bisect step start
        logger.context.update({
            'job_id': job_id,
            'metric': metric,
            'mid_point': mid_point,
            'direction': direction,
            'session_id': actual_session_id
        })

        logger.info("=== Starting Mid-Point Bisect Step ===")
        logger.info(f"Parameters: job_id={job_id}, metric={metric}, mid_point={mid_point}, direction={direction}")
        logger.info(f"Session: {actual_session_id}")

        # Initialize GitBisect with mid_point logic
        gitbisect = GitBisect(logger=logger)
        gitbisect.metric = metric        # Set metric for evaluation
        gitbisect.mid_point = mid_point  # Set mid_point for evaluation
        gitbisect.direction = direction  # Set direction for evaluation
        gitbisect.set_log(result_root)

        # Create bisect runner
        bisect_runner = MidPointBisectRun(job_id, metric, mid_point, gitbisect, logger)

        # Initialize job with current commit
        job = bisect_runner.init_bisect_commit()

        # Evaluate performance and exit with appropriate code
        bisect_runner.evaluate_commit_performance(job)

    except IndexError:
        error_msg = "ERROR: Required parameters: job_id, metric, result_root, mid_point, session_id"
        print(error_msg, file=sys.stderr)
        sys.exit(BISECT_STEP_ERROR)

    except ValueError as e:
        error_msg = f"ERROR: Invalid parameter: {str(e)}"
        print(error_msg, file=sys.stderr)
        if 'logger' in locals():
            logger.error("Parameter validation failed", error=str(e))
        sys.exit(BISECT_STEP_ERROR)

    except Exception as e:
        error_msg = f"ERROR: Mid-point bisect step failed: {str(e)}"
        print(error_msg, file=sys.stderr)
        if 'logger' in locals():
            logger.exception("Mid-point bisect step execution failed", error=str(e))
        traceback.print_exc(file=sys.stderr)
        sys.exit(BISECT_STEP_ERROR)


if __name__ == "__main__":
    main()